from typing import Union, Any, Dict, Optional, final, List
from lib.redis import Redis
from lib.mongo import MongoDB
from ._queue import ModelLogQueue
import g

Key = Union[str, int]
SetData = Dict[str, Any]


class IdNotExists(Exception):
    pass


class InsertDataListIsNone(Exception):
    pass


class _Model(object):
    def __init__(
            self,
            manage=None,  # modelManage
            db_name: str = "",  # 数据表
            uni_key: str = "",  # 唯一key
            ver: str = "local",  # 本服还是跨服
            write_redis: bool = True,  # 数据是否写入redis(游戏日志等是不需要写入redis的，上层自行判断)
            expire_time: int = 1800,  # 数据过期时间(到期清除redis和内存)
    ):
        self.manage = manage
        self.db_name = db_name
        self.uni_key = uni_key
        self.ver = ver
        self._write_redis = write_redis
        self.expire_time = expire_time

    def only_key(self, key: str) -> str:
        """redis数据缓存key"""
        return f"{self.db_name}_{key}"

    @property
    def is_write_redis(self) -> bool:
        """是否写入redis"""
        return self._write_redis

    @property
    def redis(self) -> Redis:
        """根据版本获取redis实例"""
        return g.redis if self.ver == "local" else g.cross_redis

    @property
    def mongodb(self) -> MongoDB:
        return g.mdb if self.ver == "local" else g.cross_mdb

    @final
    async def write_redis(
            self,
            key: Key,
            data: SetData
    ):
        """数据写入redis"""
        if not self.is_write_redis:
            return

        await self.redis.hmset(self.only_key(key), data)

    @final
    def dataChange(
            self,
            uid: Optional[str],
            act: str,
            *args
    ):
        """数据change,实时入库"""
        data_list = []
        if act == "update":
            uni_key, set_data = args
            data_list = [{self.uni_key: uni_key}, set_data]
        elif act == "delete":
            uni_key = args[0]
            data_list = [{self.uni_key: uni_key}]
        elif act in ["insert_one", "insert_many"]:
            data_list = list(args)

        model_log = {
            "uid": uid,
            "act": act,
            "db_name": self.db_name,
            "ver": self.ver,
            "data_list": data_list
        }
        ModelLogQueue().put(model_log)

        self.onChange(uid, act, *args)

    def onChange(
            self,
            uid: Optional[str],
            act: str,
            key: Key,
            *args
    ):
        """数据发生改变，通知子类，需上层复写"""
        ...

    def join(self, uid: str):
        """检测model访问"""
        self.manage.join(uid, self)

    def update_last_time(self, uid: str):
        """更新最后一次访问时间"""
        self.manage.update_last_time(uid, self.db_name)

    def come_in(self, uid: str):
        """join和update_last_time结合"""
        self.manage.join(uid, self, come_in=True)

    @final
    async def clear(self, uid: str):
        key = self.only_key(uid)
        # 直接删除
        if self.uni_key == "uid":
            await self.redis.delete(key)
        # 删除所有相关数据
        else:
            uniKey2RedisDataKey = await self.redis.hgetall(key)
            if uniKey2RedisDataKey:
                pipeline = self.redis.new_pipeline()
                pipeline.delete(*list(uniKey2RedisDataKey.values()))
                pipeline.delete(key)
                await pipeline.execute()
        print(f"{uid} clear [{self.db_name}] model success!")


class Model(_Model):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.cache = Cache(self)  # 缓存数据

    async def get(self, key: Key, read_redis: bool = True) -> Any:
        """获取一条数据"""
        # 取内存，高效
        data = self.cache.get(key)
        if data:
            return data

        # 从redis取
        if read_redis:
            data = await self.redis.hgetall(self.only_key(key))
            # 取不到，从mongo取
            if not data:
                data = await self.get(key, read_redis=False)
                if data:
                    # _id在gets处理
                    if self.uni_key == "uid":
                        self.join(key)
                    # 写入redis
                    await self.write_redis(key, data)
            else:
                if self.uni_key == "uid":
                    self.update_last_time(key)
        else:
            data = await self.mongodb.find1(self.db_name, {self.uni_key: key})
            if data and "_id" in data:
                data["_id"] = str(data["_id"])

        return data

    async def set(self, uid: str, key: str, data: SetData):
        """设置数据"""
        data["_lt"] = g.C.NOW()
        if self.is_write_redis:
            self.come_in(uid)
            await self.write_redis(key, data)
        self.dataChange(uid, "update", key, data)

    async def delete(self, uid: str, key: Key):
        if self.is_write_redis:
            self.come_in(uid)
            # 检测是唯一数据还是多条数据,若是多条，需要删除对应list中key
            many = False if self.uni_key == "uid" else True
            if many:
                await self.redis.hdel(self.only_key(uid), key)

            # 删除数据本身
            await self.redis.delete(self.only_key(key))

        self.dataChange(uid, "delete", key)

    async def gets(self, uid, read_redis: bool = True):
        """获取多条数据"""
        # 从redis取
        if read_redis:
            uniKey2RedisDataKey = await self.redis.hgetall(self.only_key(uid))
            # 从缓存中取出所有数据
            if uniKey2RedisDataKey:
                self.update_last_time(uid)
                pipeline = self.redis.new_pipeline()
                data_list = []
                for redisDataKey in uniKey2RedisDataKey.values():
                    pipeline.hgetall(redisDataKey)
                res = await pipeline.execute()
                if res:
                    data_list = res
            # 取不到，去mongo查
            else:
                data_list = await self.mongodb.find(self.db_name, {"uid": uid})
                if data_list:
                    pipeline = self.redis.new_pipeline()
                    set_data = {}
                    for data in data_list:
                        key = data["_id"] = str(data["_id"])
                        # 记录需要加入到list中的key
                        set_data[key] = self.only_key(key)

                        # 设置每个data的数据
                        pipeline.hmset(self.only_key(key), data)

                    # key设置到玩家的list中
                    pipeline.hmset(self.only_key(uid), set_data)
                    await pipeline.execute()

                    self.join(uid)

        # 查mongo
        else:
            data_list = await self.mongodb.find(self.db_name, {"uid": uid})

        return data_list

    async def insertOne(self, uid: str, data: Dict):
        """插入一条数据"""
        if "_id" not in data:
            raise IdNotExists("通过model插入的数据必须包含_id字段")
        if self.is_write_redis:
            self.come_in(uid)
            if self.uni_key == "uid":
                key = data["uid"]
            # 多条数据，加入list
            else:
                key = data["_id"]
                set_data = {
                    key: self.only_key(key)
                }
                await self.write_redis(uid, set_data)

            await self.write_redis(key, data)

        self.dataChange(uid, "insert_one", data)

    async def insertMany(self, uid: str, data_list: List[Dict]):
        """插入多条数据 管道批量写入"""
        if not data_list:
            raise InsertDataListIsNone("插入的数据不可为空")

        if self.is_write_redis:
            self.come_in(uid)
            set_data = {}
            for data in data_list:
                if self.uni_key == "uid":
                    key = data["uid"]
                else:
                    key = data["_id"]
                    set_data[key] = self.only_key(key)

                await self.write_redis(key, data)

            # 多条数据，加入list
            if set_data:
                await self.write_redis(uid, set_data)

        self.dataChange(uid, "insert_many", data_list)


class Cache(object):
    def __init__(self, model: Model):
        self.model = model
        self.data = {}

    def get(self, key: str):
        """获取数据"""
        return self.data.get(key)

    def set(self, key, data):
        """设置数据"""

    def delete(self, key):
        """删除key"""

    def chk_clear(self):
        """检测过期数据清除"""
        pass
